Ma doctrine d'artisan développeur


Journaux liées à cette note :

Faut-il encore configurer du swap en 2025, même sur des serveurs avec beaucoup de RAM ? #OnMePoseLaQuestion, #DevOps, #admin-sys, #linux, #JaiDécouvert, #JaiLu

Aujourd'hui, j'ai implémenté des tests de montée en charge à l'aide de Grafana k6. En ciblant un site web hébergé sur un petit serveur Scaleway DEV1-M, j'ai constaté que le serveur est devenu inaccessible à la fin des tests. Aucun swap n'était configuré sur cette Virtual machine de 4Go de RAM.

Je me suis souvenu qu'en 2019, j'ai rencontré aussi des problèmes de freeze sur une VM AWS EC2 que j'ai corrigés en ajoutant un peu de swap au serveur. Après cela, je n'ai constaté plus aucun freeze de VM pendant 4 ans.

Ce sujet de swap m'a fait penser à la question qu'un ami m'a posée en octobre 2024 :

Désactiver le swap sur une Debian, recommandé ou pas ?

Alors que j'ai 29Go utilisé sur 64, le swap était plein (3,5Go occupé à 100%), les 12 cœurs du serveur partaient dans les tours. J'ai désactivé le swap et me voilà gentiment avec un load average raisonnable, pour les tâches de cette machine.

C'est une très bonne question que je me pose depuis longtemps. J'ai enfin pris un peu de temps pour creuser ce sujet.

Sept mois plus tard, voici ma réponse dans cette note 😉.

#JaiDécouvert le paramètre kernel nommé Swappiness.

swappiness

This control is used to define how aggressive the kernel will swap memory pages. Higher values will increase aggressiveness, lower values decrease the amount of swap. A value of 0 instructs the kernel not to initiate swap until the amount of free and file-backed pages is less than the high water mark in a zone.

The default value is 60.

documentation du kernel

Dans la documentation SwapFaq d'Ubuntu j'ai lu :

The swappiness parameter controls the tendency of the kernel to move processes out of physical memory and onto the swap disk. Because disks are much slower than RAM, this can lead to slower response times for system and applications if processes are too aggressively moved out of memory.

  • swappiness can have a value of between 0 and 100
  • swappiness=0 tells the kernel to avoid swapping processes out of physical memory for as long as possible
  • swappiness=100 tells the kernel to aggressively swap processes out of physical memory and move them to swap cache

The default setting in Ubuntu is swappiness=60. Reducing the default value of swappiness will probably improve overall performance for a typical Ubuntu desktop installation. A value of swappiness=10 is recommended, but feel free to experiment. Note: Ubuntu server installations have different performance requirements to desktop systems, and the default value of 60 is likely more suitable.

source

D'après ce que j'ai compris, plus swappiness tend vers zéro, moins le swap est utilisé.

J'ai lu ici :

vm.swappiness = 60 : Valeur par défaut de Linux : à partir de 40% d’occupation de Ram, le noyau écrit sur le disque.

source

Cependant, je n'ai pas trouvé d'autres sources qui confirment cette correspondance entre la valeur de swappiness et un pourcentage précis d'utilisation de la RAM.

J'ai ensuite cherché à savoir si c'était encore pertinent de configurer du swap en 2025, sur des serveurs qui disposent de beaucoup de RAM.

#JaiLu ce thread : "Do I need swap space if I have more than enough amount of RAM?", et voici un extrait qui peut servir de conclusion :

In other words, by disabling swap you gain nothing, but you limit the operation system's number of useful options in dealing with a memory request. Which might not be, but very possibly may be a disadvantage (and will never be an advantage).

source

Je pense que ceci est d'autant plus vrai si le paramètre swappiness est bien configuré.

Concernant la taille du swap recommandée par rapport à la RAM du serveur, la documentation de Ubuntu conseille les ratios suivants :

       RAM      Swap    Maximum Swap
      256MB    256MB           512MB 
      512MB    512MB          1024MB
     1024MB   1024MB          2048MB
        1GB      1GB             2GB
        2GB      1GB             4GB
        3GB      2GB             6GB
        4GB      2GB             8GB
        5GB      2GB            10GB
        6GB      2GB            12GB
        8GB      3GB            16GB
       12GB      3GB            24GB
       16GB      4GB            32GB
       24GB      5GB            48GB
       32GB      6GB            64GB
       64GB      8GB           128GB
      128GB     11GB           256GB
      256GB     16GB           512GB
      512GB     23GB             1TB
        1TB     32GB             2TB
        2TB     46GB             4TB
        4TB     64GB             8TB
        8TB     91GB            16TB

source

#JaiDécouvert aussi que depuis le kernel 2.6, les fichiers de swap sont aussi rapides que les partitions de swap :

Definitely not. With the 2.6 kernel, "a swap file is just as fast as a swap partition."

source

Suite à ces apprentissages, j'ai configuré et activé un swap de 2G sur la VM Scaleway DEV-1L équipée de 4G de RAM, avec le paramètre swappiness réglé à 10.

J'ai relancé mon test Grafana k6 et je n'ai constaté plus aucun freeze, je n'ai pas perdu l'accès au serveur.

De plus, probablement grâce au paramètre swappiness fixé à 10, j'ai observé que le swap n'a pas été utilisé pendant le test.

Suite à ces lectures et à cette expérience concluante, j'ai décidé de désormais configurer systématiquement du swap sur tous mes serveurs de la manière suivante :

if swapon --show | grep -q "^/swapfile"; then
    echo "Swap is already configured"
else
    get_swap_size() {
        local ram_gb=$(free -g | awk '/^Mem:/ {print $2}')
        
        # Why this values? See https://help.ubuntu.com/community/SwapFaq#How_much_swap_do_I_need.3F
        if [ $ram_gb -le 1 ]; then
            echo "1G"
        elif [ $ram_gb -le 2 ]; then
            echo "1G"
        elif [ $ram_gb -le 6 ]; then
            echo "2G"
        elif [ $ram_gb -le 12 ]; then
            echo "3G"
        elif [ $ram_gb -le 16 ]; then
            echo "4G"
        elif [ $ram_gb -le 24 ]; then
            echo "5G"
        elif [ $ram_gb -le 32 ]; then
            echo "6G"
        elif [ $ram_gb -le 64 ]; then
            echo "8G"
        elif [ $ram_gb -le 128 ]; then
            echo "11G"
        else
            echo "11G"
        fi
    }

    SWAP_SIZE=$(get_swap_size)
    fallocate -l $SWAP_SIZE /swapfile
    chmod 600 /swapfile
    mkswap /swapfile
    swapon /swapfile

    if ! grep -q "^/swapfile.*swap" /etc/fstab; then
        echo "/swapfile none swap sw 0 0" >> /etc/fstab
    fi
fi

# Why 10 instead default 60? see https://help.ubuntu.com/community/SwapFaq#:~:text=a%20value%20of%20swappiness%3D10%20is%20recommended
echo 10 | tee /proc/sys/vm/swappiness
echo "vm.swappiness=10" | tee -a /etc/sysctl.conf

Nom et arborescence de Monorepo #monorepo, #software-engineering

Je suis un adepe du paradigme Monorepo, de la documentation colocalisée et des issues colocalisées.

Je conseille de nommer le repository avec le nom de l'organisation.

Par exemple, si mon organisation se nomme « Dummy Tech » et si elle utilise GitLab, alors je conseille d'utiliser le slug dummy-tech, ce qui donne gitlab.com/dummy-tech/dummy-tech/ et « Dummy Tech Monorepo » comme titre de repository.

La neutralité de ce nom facilite les décisions concernant ce qui peut ou non être inclus dans le repository, sans être limité par un nom trop restrictif. Il est flexible face à l'évolution du projet. Il permet d'éviter bien des débats à propos du nommage.

En 2018, sur GitHub, j'ai découvert un exemple de monorepo d'une organisation. Cet exemple m'a servi de base et je l'ai fait évoluer quand je travaillais chez Spacefill.

Voici un exemple d'arborescence de monorepo que j'aime utiliser :

dummy-tech
├── deployments
│   ├── prod
│   └── sandbox
├── ci
├── docs
├── playgrounds
│   ├── playground_a
│   └── playground_b
├── services
│   ├── service_a
│   ├── service_b
│   └── service_c
└── tools
    ├── tool_a
    ├── tool_b
    └── tool_c

J'ai publié le projet "pg_back-docker-sidecar" #iteration, #projet, #postgresql, #backup, #JaimeraisUnJour

Je viens de terminer une première itération de travail sur Projet 27 - "Créer un POC de pg_back".

Le résultat se trouve dans le repository GitHub : pg_back-docker-sidecar

J'ai passé en tout 17 h 30 sur ce projet, écriture de notes incluse.

Ce projet a évolué par rapport à mon objectif initial :

Initialement, dans ce dépôt, je voulais tester l'implémentation de pg_back déployé dans un conteneur Docker comme un « sidecar » pour sauvegarder une base de données PostgreSQL déployée via Docker.

Et progressivement, j'ai changé l'objectif de ce projet. Il contient maintenant

source

Voici tous les éléments testés dans le tutoriel :

  • pg_back est dépolyé dans un Docker sidecar
  • L'instance PostgreSQL est sauvegardée dans une instance Minio
  • Les archives sont chiffrées avec age
  • Les archives sont générées au format custom
  • J'ai documenté une méthode pour télécharger une archive dans un dossier du workspace du développeur
  • J'ai documenté une méthode pour restaurer l'archive dans un serveur PostgreSQL déployé via Docker
  • J'ai testé le fonctionnement du système d'expiration des archives
  • J'ai testé la fonctionnalité de "purge" automatique

Éléments que j'ai implémentés

L'image Docker proposée par pg_back ne contient pas de scheduler de type cron et ne suit pas les recommandations The Twelve-Factors App.

J'ai décidé d'implémenter ma propre image Docker stephaneklein/pg_back-docker-sidecar:2.5.0-delete-local-file-after-upload avec les ajouts suivants :

  • Support de configuration basé sur des variables d'environnement, par exemple :
  pg_back:
    image: stephaneklein/pg_back-docker-sidecar:2.5.0-delete-local-file-after-upload
    environment:
      POSTGRES_HOST: postgres1
      POSTGRES_PORT: 5432
      POSTGRES_USER: postgres
      POSTGRES_DBNAME: postgres
      POSTGRES_PASSWORD: password
      
      BACKUP_CRON: ${BACKUP_CRON:-0 3 * * *}
      UPLOAD: "s3"
      UPLOAD_PREFIX: "foobar"
      ...
  • Intégration de Supercronic pour exécuter pg_back régulièrement, une fonctionnalité de type cron

Patch envoyé en upstream

J'ai proposé deux patchs à pg_back :

Le premier patch est totalement mineur.

Dans la version actuelle 2.5.0 de pg_back, les archives dump ne sont pas supprimées du filesystem de container après l'upload vers l'Object Storage.
Ce choix me perturbe, car je préfère éviter de surcharger le disque avec des fichiers d'archives volumineux qui risquent de saturer l'espace disponible.

Pour éviter cela, j'ai implémenté "Add the --delete-local-file-after-upload to delete local file after upload" qui permet de supprimer les fichiers intermédiaires après upload.

Bilan

J'ai réussi à effectuer un cycle complet de la sauvegarde à la restauration.
J'ai décidé d'utiliser pg_back pour mes sauvegardes PostgreSQL automatique vers Object Storage.

J'ai déprécié le projet restic-pg_dump-docker pour inviter à utiliser pg_back.

Idée d'amélioration

#JaimeraisUnJour créer et implémenter les issues suivantes.

1. Implémenter une commande pg_back snapshots pour lister les snapshots sous une forme facilement lisible par un humain. Actuellement, le retour de la commande ressemble à ceci :

$ pg_back --list-remote s3
foobar/hba_file_2025-04-14T14:58:08Z.out.age
foobar/hba_file_2025-04-14T14:58:39Z.out.age
foobar/ident_file_2025-04-14T14:58:08Z.out.age
foobar/ident_file_2025-04-14T14:58:39Z.out.age
foobar/pg_globals_2025-04-14T14:58:08Z.sql.age
foobar/pg_globals_2025-04-14T14:58:39Z.sql.age
foobar/pg_settings_2025-04-14T14:58:08Z.out.age
foobar/pg_settings_2025-04-14T14:58:39Z.out.age
foobar/postgres_2025-04-14T14:58:08Z.dump.age
foobar/postgres_2025-04-14T14:58:39Z.dump.age

Je ne trouve pas ce rendu agréable à lire. J'aimerais afficher quelque chose qui ressemble à la sortie de restic. Par exemple :

$ pg_back snapshots
ID        Date                 Folder
---------------------------------------
40dc1520  2025-04-14 14:58:08  foobar
79766175  2025-04-14 14:58:39  foobar

2. Implémenter un système de suppressions des archives basé sur des règles plus avancées, comme celle de restic

3. Implémenter un refactoring vers cobra pour utiliser des sous-commandes (subcommands) et éviter le mélange entre paramètres et commandes.

Journal du samedi 22 mars 2025 à 10:58 #project-management, #gestion-projet, #software-engineering, #Doctrine

Voici quelques principes qui me guident. Je pense qu'ils contribuent à rendre une organisation efficace et efficiente.

1. La loi empirique de Gall :

« Un système complexe qui fonctionne se trouve invariablement avoir évolué depuis un système simple qui fonctionnait.
La proposition inverse se révèle également exacte : Un système complexe développé de A à Z ne fonctionne jamais et vous n'arriverez jamais à le faire fonctionner. Vous devez recommencer depuis le début, en commençant par un système simple. »

source

2. Je suis convaincu de la pertinence du modèle de Tuckman pour comprendre comment les équipes se construisent et évoluent au fil du temps. En conséquence, je crois qu'une organisation doit accorder suffisamment de temps aux équipes pour qu'elles atteignent leur phase de performance, puis veiller à maintenir la composition de l'équipe sur la durée.

3. Le modèle du triangle de gestion de projet qui je trouve est très bien expliqué dans le livre Getting Real :

Voici un moyen simple de lancer le projet dans les délais et le budget impartis : ne pas les modifier. Il ne faut jamais consacrer plus de temps ou d'argent à un problème, mais simplement en réduire la taille (le périmètre).

Il existe un mythe qui dit que l'on peut lancer un projet dans les délais, en respectant le budget et le champ d'application. Cela n'arrive presque jamais et, lorsque c'est le cas, la qualité s'en ressent souvent.

Si vous ne pouvez pas tout faire tenir dans le temps et le budget impartis, n'augmentez pas le temps et le budget. Au contraire, réduisez le champ d'application. Il sera toujours temps d'ajouter des choses plus tard - plus tard est éternel, maintenant est éphémère.

Il vaut mieux lancer quelque chose d'excellent dont la portée est un peu plus réduite que prévu que de lancer quelque chose de médiocre et plein de trous parce qu'il fallait respecter une fenêtre magique de temps, de budget et de portée. Laissez la magie à Houdini. Vous avez une véritable entreprise à gérer et un véritable produit à livrer.

source

Journal du jeudi 27 février 2025 à 10:41 #golang, #DevOps, #selfhosting, #JaiDécouvert, #JaimeraisUnJour

En travaillant sur la note "Je découvre Beszel", #JaiDécouvert ici un autre projet de monitoring intéressant : Dozzle (https://dozzle.dev/).

Dozzle is a small lightweight application with a web based interface to monitor Docker logs. It doesn’t store any log files. It is for live monitoring of your container logs only.

source

Là aussi, Dozzle est un projet en Golang, commencé fin 2018.

Dozzle est une alternative à Loki + Grafana.

#JaimeraisUnJour déployer Dozzle pour le tester et si ce test est concluant, je l'intégrerai peut-être à ma stack minimaliste de monitoring en complément de Beszel et Gatus.

Je découvre Beszel #JaiDécouvert, #OnMaPartagé, #monitoring, #DevOps, #Doctrine

Un ami m'a partagé le projet Beszel (https://beszel.dev/).

Beszel is a lightweight server monitoring platform that includes Docker statistics, historical data, and alert functions.

It has a friendly web interface, simple configuration, and is ready to use out of the box. It supports automatic backup, multi-user, OAuth authentication, and API access.

source

Beszel est codé en Golang, il est très récent, il a commencé en été 2024, c'est sans doute pour cela que je ne l'avais jamais croisé.

De prime abord, j'ai pensé que Beszel était un outil de Status / Uptime pages comme Uptime Kuma ou Gatus, mais ce n'est pas le cas.

Je qualifierai plutôt Beszel d'alternative "plug and play" de Prometheus + Grafana + node_exporter + cAdvisor.

Alors que l'annonce de Beszel a fait "choux blanc" sur Hacker News « Beszel: Lightweight server resource monitoring with history, Docker stats,alerts », le projet a suscité plus de réaction — 270 commentaires — sur Subreddit SelfHosted : « I just released Beszel, a server monitoring hub with historical data, docker stats, and alerts. It's a lighter and simpler alternative to Grafana + Prometheus or Checkmk. Any feedback is appreciated! ».

Les retours sont très positifs 🙂 :

« There is beauty in simplicity. Very nice little application! »

« Kiss »

« Just installed on all of my servers, gorgeous project, simple but also not simple. »

« Awesome work. I think you identified a good use case for the self hosting community, a simple server monitor running as a simple service. I will give it a go soon! »

« I never installed Grafana and Prometheus because it’s overkill for my little server.. but this looks really good! I’ll give it a go »

Prometheus propose bien plus d'exporter que Beszel, mais je pense que Beszel est un bon point de départ pour une stack de monitoring minimaliste.

À l'avenir, mon choix par défaut en matière monitoring sera probablement un couple Beszel + Gatus. Si des besoins plus avancés émergent, comme du monitoring poussé de PostgreSQL, Redis ou d'autres services, j'envisagerai alors de commencer la mise en place du couple Prometheus + Grafana.

Journal du mardi 25 février 2025 à 09:35 #Doctrine, #documentation, #shell, #terminal, #dev-kit, #markdown

Dans cette note, je souhaite décrire et expliquer comment j'intègre des sessions de terminal dans des documentations.

Convention mainstream que j'ai adoptée

Je suis la convention mainstream suivante pour représenter des sessions terminal (de type bash, zsh…) :

Première partie :

$ echo "Hello, world!"
Hello, world!

$ ls -l
total 0
-rw-r--r-- 1 user user 0 Feb 25 10:00 file.txt

Seconde partie :

$ sudo su
# systemctl restart nginx 
$ exit

$ uptime # Affiche l'uptime
uptime 10:00:00 up 1 day, 2:45, 1 user, load average: 0.15, 0.10, 0.05

Les lignes commençant par $ ou # indiquent les commandes saisies par l'utilisateur, tandis que les autres correspondent aux sorties du terminal.
Il m'arrive également d'ajouter des commentaires en fin de ligne à l'aide de # ….

Origine de cette convention ?

J'ai cherché à retracer l'origine de cette pratique et elle semble très ancienne, probablement adoptée dès les débuts d'Unix en 1971.
On retrouve cette syntaxe notamment dans le livre The Unix Programming Environment (pdf), publié en 1984.


Ne pas inclure de $ si un bloc ne contient aucune sortie de commande

La règle MD014 de Markdownlint aborde ce sujet :

MD014 - Dollar signs used before commands without showing output

Tags: code

Aliases: commands-show-output

This rule is triggered when there are code blocks showing shell commands to be typed, and the shell commands are preceded by dollar signs ($):

$ ls
$ cat foo
$ less bar

The dollar signs are unnecessary in the above situation, and should not be included:

ls
cat foo
less bar

However, an exception is made when there is a need to distinguish between typed commands and command output, as in the following example:

$ ls
foo bar
$ cat foo
Hello world
$ cat bar
baz

Rationale: it is easier to copy and paste and less noisy if the dollar signs are omitted when they are not needed. See https://cirosantilli.com/markdown-style-guide#dollar-signs-in-shell-code for more information.

source

Pendant un certain temps, j’ai suivi la recommandation de ne pas inclure de $ dans un bloc de commande lorsqu’il n’affiche aucune sortie.

Après quelques années, j’ai décidé de ne plus appliquer cette règle pour la raison suivante :

  • Lorsque la documentation mélange des fichiers de configuration et des commandes sans $, j’ai du mal à distinguer ce qui doit être exécuté dans un terminal et ce qui relève de la configuration.

Pour éviter toute ambiguïté, j’ai adopté une approche radicale : toujours utiliser $ pour indiquer une commande à exécuter, même si le bloc ne contient pas de sortie.

Je n'aime pas les $ dans les blocs de commande, car que je ne peux pas copier / coller facilement !

Je comprends tout à fait cette remarque. Cependant, ces "screenshots" de session de terminal n'ont pas vocation à être copiées / collées en masse.
Ils ont avant tout une fonction explicative.
Ce sont des sortes de "screenshot".

Si j'éprouve le besoin de faire souvent des "copier / coller" d'exemples de session terminal, alors je considère que c'est le signe qu'un script "helper" serait plus approprié pour exécuter ce groupe de commandes d’un seul coup.

Par exemple, je peux remplacer ce contenu :

Ensure that the following prerequisites have been installed: mise and direnv, see instruction in ../README.md.

$ mise trust
$ mise install
$ scw version
Version    2.34.0

Par celui-ci (qui indique l'existence du script ./scripts/mise-install.sh) :

Ensure that the following prerequisites have been installed: mise and direnv, see instruction in ../README.md (or execute ./scripts/mise-install.sh).

$ mise trust
$ mise install
$ scw version
Version    2.34.0

Cela évite les copier-coller répétitifs tout en conservant la clarté de l’explication.

En gestion de projet logiciel, quelle est la définition de "Theme" ? #software-engineering, #scrum, #gestion-projet, #vocabulaire, #JaiDécidé

J'ai du mal à bien définir le terme thème en gestion de projet logiciel.

Dans cette note, je vais décrire les deux définitions que je connais.


Voici une définition de Ken Rubin, auteur du livre Essential Scrum :

A collection of related user stories. A theme provides a convenient way to indicate that a set of stories have something in common, such as being in the same functional area.

Dans la section glossaire de Essential Scrum

Un Theme peut-être assigné à tout type d'issue, par exemple Epic ou User Story.


Les auteurs du livre Product Roadmaps Relaunched ont une autre définition de Theme :

As we’ve touched on, in the relaunched roadmapping process we use themes and subthemes to express customer needs. This is probably a new concept for many of you, so let’s define what we mean by these terms.

Themes are an organizational construct for defining what’s important to your customers at the present time.

...

So, again, themes and subthemes represent the needs and problems your product will solve for. A need is generally something the customer doesn’t have yet, whereas a problem is something that’s not working right (with the existing product, or whatever substitute they might currently be using). Even though these two terms suggest subtle differences, the important point is that both refer to a gap or pain in the customer’s experience. When identifying the themes and subthemes for your roadmap, remember to consider both needs and problems from all angles.

Jared Spool, paraphrasing our very own Bruce McCarthy, says, “Themes help teams stay focused without prematurely committing to a solution that may not be the best idea later on.” As Spool points out, it is important to focus most of the roadmapping effort on customer needs and problems because “the viability of a feature may shift dramatically, while the nature of an important customer problem will likely remain the same.”

Page 124 du livre Product Roadmaps Relaunched

Dans ce livre, un Theme représente un besoin client important à un instant donné.


D'après ce que j'ai compris, Product Roadmaps Relaunched adopte une définition plus restrictive du concept de thème que Essential Scrum. Dans Product Roadmaps Relaunched, un thème sert uniquement à décrire des fonctionnalités de façon imprécise.

Exemple tiré du livre Product Roadmaps Relaunched :

  • Theme: Billing & payments
    • Subtheme: Billing & payments API integration
    • Subtheme: API integration testing

Pour résumer :

  • Essential Scrum inclut des thèmes qui ne sont pas seulement liés aux fonctionnalités, mais aussi à la stratégie globale, à l’organisation, voire aux aspects techniques et processus internes.
  • Product Roadmaps Relaunched reste focalisé sur l'expérience utilisateur et les besoins clients, avec des thèmes qui expriment des fonctionnalités sans trop rentrer dans les considérations techniques ou organisationnelles.

#JaiDécidé d'adopter la définition de "Theme" donnée dans Essential Scrum.

Workflow de gestion des secrets d'un projet basé sur Age et des clés ssh #Doctrine, #chiffrement, #password, #secret, #dev-kit

Cette note est aussi disponible au format vidéo : https://www.youtube.com/watch?v=J9jsd4m9YkQ.

De 2018 à 2023, j'utilisais l'outil "pass" (https://www.passwordstore.org/) couplé avec GNU Privacy Guard pour chiffrer et déchiffrer les secrets dans mes projets professionnels.

Bien que cette méthode fonctionnait, elle s'avérait laborieuse.
La procédure documentant l'installation, la création des clés et la configuration de gnupg était l'étape posant le plus de difficultés d'onboarding des développeurs sur ces projets.
Au feeling, je dirais que trois développeurs sur quatre bloquaient à cette étape.

En parallèle à cette méthode, j'ai essayé d'utiliser la cli de Bitwarden, mais l'expérience n'a pas été très concluante, principalement à cause de problèmes de latence lors de son exécution.

Aujourd'hui, j'ai exploré une nouvelle méthode basée sur age et son support natif des clés ssh.

Voici le repository Git du résultat de cette exploration : age-secret-skeleton.

Le résultat est très positif et je pense avoir trouvé ma nouvelle méthode de chiffrement des secrets dans mes projets 🙂.

Pour en savoir plus, vous pouvez à la fois suivre le README.me du repository et continuer la lecture de cette note.

Détail des fichiers du repository

.
├── .envrc
├── .gitignore
├── .mise.toml
├── README.md
├── scripts
│   ├── decrypt_secrets.sh
│   └── encrypt_secrets.sh
├── .secret
├── .secret.age
├── .secret.skel
└── ssh-keys
    ├── john-doe.pub
    └── stephane-klein.pub
  • /.secret : fichier qui contient les secrets en clair. Ce fichier est ignoré par .gitignore afin qu'il ne soit pas présent dans le repository git
  • /.secret.age : fichier qui contient le contenu du fichier .secret chiffré avec age. Ce fichier est ajouté au repository git.
  • /ssh-keys/ : ce dossier contient les clés publiques ssh des personnes qui ont accès au contenu de .secret.age
  • /scripts/decrypt_secrets.sh : permet de déchiffrer avec age le contenu de .secret.age et écrit le résultat en clair dans .secret
  • /scripts/encrypt_secrets.sh : permet de chiffrer avec age le contenu de .secret vers .secret.age. Ce script doit être exécuté quand le contenu de .secret est modifié ou quand une nouvelle clé est ajoutée dans /ssh-keys/
  • /.envrc : Dans ce skeleton, ce fichier charge uniquement les variables d'environnement présentes dans .secret mais dans un vrai projet, il permet de charger automatiquement des variables d'environnement qui ne sont pas des secrets.
  • /.secret.skel : ce fichier contient l'exemple de contenu du fichier .secret, sans les secrets. Par exemple :
# You can either request these secrets from Stéphane Klein (contact@stephane-klein.info)
# or, if your public ssh key is present in `./ssh-keys/` use the ./scripts/decrypt_secrets.sh command
# which will automatically decrypt the .secret.age file to .secret

export POSTGRES_PASSWORD="..."
export SCW_SECRET_KEY="..."

Ce fichier n'est pas nécessaire au bon fonctionnement du workflow mais je le trouve utile pour :

  • documenter le contenu de .secret aux personnes qui ont juste accès au dépôt Git
  • permettre de review les modifications de .secret.skel dans des Merge Request.

Utilisation des clés ssh

L'un des avantages majeurs de age par rapport à pass + gnupg est son support natif des clés SSH pour chiffrer et déchiffrer les secrets.

Ainsi, je n'ai plus besoin de demander aux développeurs deux types de clés (SSH et gnupg) : une seule clé SSH suffit.

De plus, il me semble qu'un grand nombre de développeurs possèdent déjà une clé SSH, alors que je pense que gnupg reste une technologie bien moins répandue.

Détail d'implémentation des scripts

Vvoici le contenu de /scripts/encrypt_secrets.sh :

#!/usr/bin/env bash
set -e

cd "$(dirname "$0")/../"

# Prepare recipient arguments for age
recipient_args=()
for pubkey in ./ssh-keys/*.pub; do
    if [ -f "$pubkey" ]; then
        recipient_args+=("-R" "$pubkey")
    fi
done

# Execute age with all public keys
age "${recipient_args[@]}" -o .secret.age .secret

La boucle permet de passer toutes les clés ssh du dossier /ssh-keys/* en paramètre d' age.

Voici le contenu de /scripts/decrypt_secrets.sh :

#!/usr/bin/env bash
set -e

cd "$(dirname "$0")/../"

# Prepare identity arguments for age
identity_args=()
for key in ~/.ssh/id_*; do
    if [ -f "$key" ] && ! [[ "$key" == *.pub ]]; then
        identity_args+=("-i" "$key")
    fi
done

# Execute age with all identity files
age -d "${identity_args[@]}" -o .secret .secret.age

cat << EOF
Secret decrypted in .secret
Don't forget to run the command:

$ source .envrc
EOF

La boucle permet de passer en paramètre toutes les clés privées ssh du dossier ~/.ssh/ en espérant en trouver une qui correspond à une clé publique du dossier /ssh-keys/.

Plusieurs niveaux de sécurisation

Si je veux cloisonner les secrets en limitant leur accès à des groupes d'utilisateurs distincts, je peux utiliser des secrets différents selon l'environnement.

Par exemple :

.
├── production
│   ├── .secret.age
│   └── ssh-keys
└── sandbox
    ├── .secret.age
    └── ssh-keys

Cette structure me permet de donner à certains utilisateurs accès aux secrets de l'environnement sandbox, sans leur donner accès à ceux de production.

Aller plus loin avec par exemple Vault ?

Je pense qu'il est possible d'aller plus loin en matière de sécurité avec des solutions comme Vault, mais trouve que la méthode basée sur age reste plus simple à déployer dans une petite équipe.

Journal du mercredi 12 février 2025 à 11:41 #network, #unix, #linux, #JaiDécouvert

En rédigeant la note 2025-02-12_1044, je me suis demandé quelle est la différence de performance entre un Unix Socket et un INET socket.

Durant mes recherches, j'ai découvert qu'Unix Socket est plus rapide que INET sockets. Je n'ai pas été surpris, j'avais cette intuition.

Toutefois, j'ai été surpris d'apprendre que le gain est non négligeable, de 30% à 50% de gains !

Exemple :

On my machine, Ryzen 3900X, Ubuntu 22,

A basic C++ TCP server app that only sends 64K packets, and a basic c++ receiver that pulls 100GB of these packets and copies them to it's internal buffer only, single-threaded:

  • achieves ~30-33 GBit/sec for TCP connection (~4.0GB/sec) (not MBit)
  • and ~55-58GBit/sec for a socket connection, (~7.3 GB/sec)
  • and ~492Gbit/sec for in-process memcopy (~61GB /sec)

source

Conséquence : je vais essayer d'utiliser des Unix sockets autant que je peux.